/**
 * Copyright (C) 2010-2011 J.W.Marsden
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/
package cc.plural.jsonij.marshal;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import cc.plural.jsonij.marshal.InspectorProperty.TYPE;
import cc.plural.jsonij.marshal.annotation.JSONAccessor;
import cc.plural.jsonij.marshal.annotation.JSONIgnore;
import cc.plural.jsonij.marshal.annotation.JSONMutator;
import cc.plural.jsonij.marshal.annotation.JSONName;

/**
 *
 * @author openecho
 */
public class Inspector {

    public static final String IS_PREFIX;
    public static final String SET_PREFIX;
    public static final String GET_PREFIX;
    Object o;
    Class<?> c;
    InspectorProperty[] properties;
    InspectorFilter filter;
    boolean innerArray;
    boolean innerObject;

    static {
        IS_PREFIX = "is";
        SET_PREFIX = "set";
        GET_PREFIX = "get";
    }

    public Inspector(Class<?> c) {
        this.c = c;
        this.o = null;
        properties = null;
        filter = InspectorFilter.getDefaultFilters();
        innerArray = false;
        innerObject = false;
    }

    public Inspector(Object o) {
        this.o = o;
        properties = null;
        filter = InspectorFilter.getDefaultFilters();
        innerArray = false;
        innerObject = false;
    }

    public Object getC() {
        return c;
    }

    public Object getO() {
        return o;
    }

    public boolean hasInnerArray() {
        return innerArray;
    }

    public boolean hasInnerObject() {
        return innerObject;
    }
    
    public boolean hasProperty(String name) {
        for(InspectorProperty property: properties) {
            if(property.getPropertyName().equals(name)) {
                return true;
            }
        }
        return false;
    }
    
    public InspectorProperty getProperty(String name) {
        for(InspectorProperty property: properties) {
            if(property.getPropertyName().equals(name)) {
                return property;
            }
        }
        return null;
    }
    

    public void inspect() {
        if (o == null && c == null) {
            return;
        }
        if (o != null) {
            c = o.getClass();
        }
        Class<?> objectClass = c;
        // Inspect Properties
        InspectorProperty[] propertyList = getMethodProperties(objectClass);
        if (propertyList != null) {
            InspectorProperty[] fieldPropertyList = getAttributeProperties(objectClass);
            if (fieldPropertyList != null) {
                HashMap<String, InspectorProperty> propertiesMap = new HashMap<String, InspectorProperty>();
                for (InspectorProperty prop : propertyList) {
                    propertiesMap.put(prop.getPropertyName(), prop);
                }
                for (InspectorProperty prop : fieldPropertyList) {
                    if (propertiesMap.containsKey(prop.getPropertyName())) {
                        InspectorProperty existingProp = propertiesMap.get(prop.getPropertyName());
                        if (!existingProp.hasAccessor()) {
                            existingProp.setAccessName(prop.getAccessName());
                            existingProp.setAccessPropertyType(TYPE.FIELD);
                        }
                        if (!existingProp.hasMutator()) {
                            existingProp.setMutateName(prop.getMutateName());
                            existingProp.setMutatePropertyType(TYPE.FIELD);
                        }
                    } else {
                        propertiesMap.put(prop.getPropertyName(), prop);
                    }
                }
                propertyList = new InspectorProperty[propertiesMap.size()];
                propertiesMap.values().toArray(propertyList);
            }
        } else {
            propertyList = getAttributeProperties(objectClass);
        }

        if (propertyList == null) {
            propertyList = new InspectorProperty[0];
        }
        properties = propertyList;

        // Inspect inner array (List type) and Inspect inner object (Map type)
        Class<?>[] interfaces = null;
        Class<?> parent = objectClass.getSuperclass();
        if (parent != null) {
            do {
                interfaces = parent.getInterfaces();
                for (int i = 0; i < Array.getLength(interfaces); i++) {
                    if (interfaces[i] == List.class) {
                        innerArray = true;
                    }
                    if (interfaces[i] == Map.class) {
                        innerObject = true;
                    }
                    if (innerArray && innerObject) {
                        break;
                    }
                }
                if (innerArray && innerObject) {
                    break;
                }
            } while (( parent = parent.getSuperclass() ) != null);
        }

    }

    public InspectorProperty[] getAttributeProperties(Class<?> objectClass) {
        InspectorProperty[] result = null;
        HashMap<String, InspectorProperty> attributeProperties = new HashMap<String, InspectorProperty>();
        Field[] objectFields = objectClass.getFields();
        for (Field field : objectFields) {
            if (isJSONIgnored(field) || filter.isFiltered(field.getDeclaringClass())) {
                continue;
            }
            InspectionData fieldInspectionData = getFieldInspectionData(field);
            if (fieldInspectionData == null || !fieldInspectionData.hasPropertyName()) {
                continue;
            }
            String propertyName = fieldInspectionData.getPropertyName();
            InspectorProperty property = null;
            if (!attributeProperties.containsKey(propertyName)) {
                property = new InspectorProperty(propertyName, TYPE.FIELD);
                attributeProperties.put(propertyName, property);
                property.setPropertyName(propertyName);
                property.setAccessName(fieldInspectionData.getName());
                property.setMutateName(fieldInspectionData.getName());
                property.setAccessReturnType(fieldInspectionData.getReturnType());
                property.setMutateInputType(fieldInspectionData.getArgumentType());
            }
        }
        if (!attributeProperties.isEmpty()) {
            result = new InspectorProperty[attributeProperties.size()];
            attributeProperties.values().toArray(result);
        }
        return result;
    }

    public InspectorProperty[] getMethodProperties(Class<?> objectClass) {
        InspectorProperty[] result = null;
        HashMap<String, InspectorProperty> methodProperties = new HashMap<String, InspectorProperty>();
        Method[] objectMethods = objectClass.getMethods();
        for (Method method : objectMethods) {
            if (isJSONIgnored(method) || filter.isFiltered(method.getDeclaringClass())) {
                continue;
            }
            InspectionData methodInspectionData = getMethodInspectionData(method);
            if (methodInspectionData == null || !methodInspectionData.hasPropertyName()) {
                continue;
            }
            String propertyName = methodInspectionData.getPropertyName();
            InspectorProperty property = null;
            if (methodProperties.containsKey(propertyName)) {
                property = methodProperties.get(propertyName);
            } else {
                property = new InspectorProperty(propertyName, TYPE.METHOD);
                methodProperties.put(propertyName, property);
            }
            if (methodInspectionData.getAccessType() == ACCESS_TYPE.ACCESS && !property.hasAccessor()) {
                property.setAccessName(methodInspectionData.getName());
                property.setAccessReturnType(methodInspectionData.getReturnType());
            } else if (methodInspectionData.getAccessType() == ACCESS_TYPE.MUTATE && !property.hasMutator()) {
                property.setMutateName(methodInspectionData.getName());
                property.setMutateInputType(methodInspectionData.getArgumentType());
            }
        }
        if (!methodProperties.isEmpty()) {
            result = new InspectorProperty[methodProperties.size()];
            methodProperties.values().toArray(result);
        }
        return result;
    }

    public InspectionData getFieldInspectionData(Field field) {
        InspectionData fieldInspectionData = new InspectionData();
        fieldInspectionData.setName(field.getName());
        fieldInspectionData.setAccessType(ACCESS_TYPE.BOTH);
        String jsonName = Inspector.getJSONName(field);
        if (jsonName != null) {
            fieldInspectionData.setPropertyName(jsonName);
        }
        if (!fieldInspectionData.hasPropertyName()) {
            fieldInspectionData.setPropertyName(fieldInspectionData.getName());
        }
        fieldInspectionData.setArgumentType(field.getType());
        fieldInspectionData.setReturnType(field.getType());
        return fieldInspectionData;
    }

    public InspectionData getMethodInspectionData(Method method) {
        InspectionData methodInspectionData = new InspectionData();
        methodInspectionData.setName(method.getName());
        String jsonName = Inspector.getJSONName(method);
        if (jsonName != null) {
            methodInspectionData.setPropertyName(jsonName);
        }
        if (!methodInspectionData.hasPropertyName()) {
            String accessorName = Inspector.getJSONAccessor(method);
            if (accessorName != null) {
                methodInspectionData.setPropertyName(accessorName);
                methodInspectionData.setAccessType(ACCESS_TYPE.ACCESS);
            }
        }
        if (!methodInspectionData.hasPropertyName()) {
            String mutatorName = Inspector.getJSONMutator(method);
            if (mutatorName != null) {
                methodInspectionData.setPropertyName(mutatorName);
                methodInspectionData.setAccessType(ACCESS_TYPE.MUTATE);
            }
        }
        methodInspectionData.setReturnType(method.getReturnType());
        if (!methodInspectionData.hasPropertyName()) {
            char ch;
            String propertyName = null;
            String methodName = methodInspectionData.getName();
            Class<?> returnType = methodInspectionData.getReturnType();
            if (methodName.length() > IS_PREFIX.length() && methodName.startsWith(IS_PREFIX) && returnType == boolean.class && Character.isUpperCase(ch = methodName.charAt(IS_PREFIX.length()))) {
                propertyName = Character.toLowerCase(ch) + methodName.substring(IS_PREFIX.length() + 1, methodName.length());
                methodInspectionData.setPropertyName(propertyName);
                methodInspectionData.setAccessType(ACCESS_TYPE.ACCESS);
            } else if (methodName.length() > SET_PREFIX.length() && methodName.startsWith(SET_PREFIX) && Character.isUpperCase(ch = methodName.charAt(SET_PREFIX.length()))) {
                propertyName = Character.toLowerCase(ch) + methodName.substring(SET_PREFIX.length() + 1, methodName.length());
                methodInspectionData.setPropertyName(propertyName);
                methodInspectionData.setAccessType(ACCESS_TYPE.MUTATE);
            } else if (methodName.length() > GET_PREFIX.length() && methodName.startsWith(GET_PREFIX) && Character.isUpperCase(ch = methodName.charAt(GET_PREFIX.length())) && returnType != null) {
                propertyName = Character.toLowerCase(ch) + methodName.substring(GET_PREFIX.length() + 1, methodName.length());
                methodInspectionData.setPropertyName(propertyName);
                methodInspectionData.setAccessType(ACCESS_TYPE.ACCESS);
            }
        }
        if (methodInspectionData.getAccessType() == ACCESS_TYPE.ACCESS) {
            Class<?> returnType = method.getReturnType();
            if (returnType != null) {
                methodInspectionData.setReturnType(returnType);
            } else {
                return null;
            }
        } else if (methodInspectionData.getAccessType() == ACCESS_TYPE.MUTATE) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (Array.getLength(parameterTypes) == 1) {
                Class<?> parameterType = parameterTypes[0];
                methodInspectionData.setArgumentType(parameterType);
            } else {
                return null;
            }
        }
        return methodInspectionData;
    }

    public InspectorProperty[] getProperties() {
        if (o != null && properties == null) {
            inspect();
        }
        return properties;
    }

    public void setProperties(InspectorProperty[] properties) {
        this.properties = properties;
    }

    public boolean isJSONIgnored(AccessibleObject object) {
        return object.getAnnotation(JSONIgnore.class) != null;
    }

    public static String getAnnotatedName(AccessibleObject object) {
        String name = getJSONName(object);
        if (name == null) {
            name = getJSONMutator(object);
        }
        if (name == null) {
            name = getJSONAccessor(object);
        }
        return name;
    }

    public static String getJSONName(AccessibleObject object) {
        String name = null;
        JSONName jsonNameAnnotation = object.getAnnotation(JSONName.class);
        if (jsonNameAnnotation != null) {
            name = jsonNameAnnotation.value();
        }
        return name;
    }

    public static String getJSONMutator(AccessibleObject object) {
        String name = null;
        JSONMutator jsonMutatorAnnotation = object.getAnnotation(JSONMutator.class);
        if (jsonMutatorAnnotation != null) {
            name = jsonMutatorAnnotation.value();
        }
        return name;
    }

    public static String getJSONAccessor(AccessibleObject object) {
        String name = null;
        JSONAccessor jsonAccessorAnnotation = object.getAnnotation(JSONAccessor.class);
        if (jsonAccessorAnnotation != null) {
            name = jsonAccessorAnnotation.value();
        }
        return name;
    }

    public enum ACCESS_TYPE {

        ACCESS, MUTATE, BOTH
    }

    public static class InspectionData {

        protected String name;
        protected String propertyName;
        protected Class<?> returnType;
        protected Class<?> argumentType;
        protected ACCESS_TYPE accessType;

        public InspectionData() {
            name = null;
            propertyName = null;
            returnType = null;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getPropertyName() {
            return propertyName;
        }

        public void setPropertyName(String propertyName) {
            this.propertyName = propertyName;
        }

        public Class<?> getReturnType() {
            return returnType;
        }

        public void setReturnType(Class<?> returnType) {
            this.returnType = returnType;
        }

        public Class<?> getArgumentType() {
            return argumentType;
        }

        public void setArgumentType(Class<?> argumentType) {
            this.argumentType = argumentType;
        }

        public boolean hasPropertyName() {
            return propertyName != null;
        }

        public ACCESS_TYPE getAccessType() {
            return accessType;
        }

        public void setAccessType(ACCESS_TYPE accessType) {
            this.accessType = accessType;
        }
    }
}
